﻿using System;
using GlmSharp;

namespace Trackball
{
    enum CameraMotionType
    {
        NONE, ARC, FIRSTPERSON, PAN, ROLL, ZOOM
    };

    class TrackBallInteractor
    {
        private Camera mCamera;
        private CameraMotionType mCameraMotionLeftClick;
        private CameraMotionType mCameraMotionMiddleClick;
        private CameraMotionType mCameraMotionRightClick;
        private CameraMotionType mCameraMotionScroll;
        private vec2 mClickPoint;
        private float mHeight;
        private bool mIsDragging;
        private bool mIsLeftClick;
        private bool mIsMiddleClick;
        private bool mIsRightClick;
        private bool mIsScrolling;
        private float mPanScale;
        private vec2 mPrevClickPoint;
        private float mRollScale;
        private float mRollSum;
        private quat mRotation;
        private quat mRotationSum;
        private float mSpeed;
        private vec3 mStartVector;
        private vec3 mStopVector;
        private float mTranslateLength;
        private float mWidth;
        private float mZoomSum;
        private float mZoomScale;

        public static readonly vec3 X = new vec3(1.0f, 0.0f, 0.0f);
        public static readonly vec3 Y = new vec3(0.0f, 1.0f, 0.0f);
        public static readonly vec3 Z = new vec3(0.0f, 0.0f, 1.0f);

        public TrackBallInteractor()
        {
            mCameraMotionLeftClick = CameraMotionType.ARC;
            mCameraMotionMiddleClick = CameraMotionType.ROLL;
            mCameraMotionRightClick = CameraMotionType.FIRSTPERSON;
            mCameraMotionScroll = CameraMotionType.ZOOM;
            mHeight = 1;
            mIsDragging = false;
            mIsLeftClick = false;
            mIsMiddleClick = false;
            mIsRightClick = false;
            mIsScrolling = false;
            mPanScale = 0.005f;
            mRollScale = 0.005f;
            mRollSum = 0.0f;
            mRotation = new quat(0.0f, 0.0f, 0.0f, 1.0f);//参数依次为X,Y,Z,W
            mRotationSum = new quat(0.0f, 0.0f, 0.0f, 1.0f);
            mSpeed = 1.0f;
            mWidth = 1;
            mZoomScale = 0.1f;
            mZoomSum = 0.0f;
            mCamera = new Camera();
        }

        protected char clickQuadrant(float x, float y)
        {
            float halfw = 0.5f * mWidth;
            float halfh = 0.5f * mHeight;

            if (x > halfw)
            {
                if (y < halfh)
                    return '1';
                else
                    return '4';
            }
            else
            {
                if (y < halfh)
                    return '2';
                else
                    return '3';
            }
        }

        protected void computeCameraEye(ref vec3 eye)
        {
            vec3 orientation = mRotationSum * Z;
            if (mZoomSum != 0.0f)
            {
                mTranslateLength += mZoomScale * mZoomSum;
                mZoomSum = 0.0f;
            }
            eye = mTranslateLength * orientation + mCamera.getCenter();
        }

        protected void computeCameraUp(ref vec3 up)
        {
            up = (mRotationSum * Y).NormalizedSafe;
        }

        protected void computePan(ref vec3 pan)
        {
            vec2 click = mClickPoint - mPrevClickPoint;
            vec3 look = mCamera.getEye() - mCamera.getCenter();
            float length = look.Length;
            vec3 right = (mRotationSum * X).NormalizedSafe;

            pan = (mCamera.getUp() * (-click.y) + right * click.x) * mPanScale * mSpeed * length;
        }

        public void computePointOnSphere(vec2 point, ref vec3 result)
        {
            float x = (2.0f * point.x - mWidth) / mWidth;
            float y = (mHeight - 2.0f * point.y) / mHeight;

            float length2 = x * x + y * y;

            if (length2 <= 0.5f)
            {
                result.z = (float)Math.Sqrt(1.0 - length2);
            }
            else
            {
                result.z = 0.5f / (float)Math.Sqrt(length2);
            }

            float norm = 1.0f / (float)Math.Sqrt(length2 + result.z * result.z);

            result.x = x * norm;
            result.y = y * norm;
            result.z *= norm;
        }

        public void computeRotationBetweenVectors(vec3 u, vec3 v, ref quat result)
        {
            float cosTheta = vec3.Dot(u, v);

            vec3 rotationAxis = new vec3(0.0f, 0.0f, 0.0f);
            const float EPSILON = 1.0e-5f;

            if (cosTheta < -1.0f + EPSILON)
            {
                rotationAxis = vec3.Cross(new vec3(0.0f, 0.0f, 1.0f), u);

                if (rotationAxis.LengthSqr < 0.01f)
                {
                    rotationAxis = vec3.Cross(new vec3(1.0f, 0.0f, 0.0f), u);
                }
                rotationAxis = rotationAxis.NormalizedSafe;
                result = quat.FromAxisAngle((float)Math.PI, rotationAxis);
            }
            else if (cosTheta > 1.0f - EPSILON)
            {
                result = new quat(0.0f, 0.0f, 0.0f, 1.0f);
            }
            else
            {
                float theta = (float)Math.Acos(cosTheta);
                rotationAxis = vec3.Cross(u, v);
                
                rotationAxis = rotationAxis.NormalizedSafe;
                result = quat.FromAxisAngle(theta * mSpeed, rotationAxis);
            }

        }

        protected void drag()
        {
            if (mPrevClickPoint == mClickPoint)
            {
                return;
            }

            computePointOnSphere(mClickPoint, ref mStopVector);
            computeRotationBetweenVectors(mStartVector, mStopVector, ref mRotation);

            mRotation = mRotation.Inverse;

            drag(mIsLeftClick, mCameraMotionLeftClick);
            drag(mIsMiddleClick, mCameraMotionMiddleClick);
            drag(mIsRightClick, mCameraMotionRightClick);

            mPrevClickPoint = mClickPoint;
            mStartVector = mStopVector;
        }

        protected void drag(bool isClicked, CameraMotionType motion)
        {
            if (!isClicked)
                return;

            switch (motion)
            {
                case CameraMotionType.ARC:
                    dragArc();
                    break;
                case CameraMotionType.FIRSTPERSON:
                    dragFirstPerson();
                    break;
                case CameraMotionType.PAN:
                    dragPan();
                    break;
                case CameraMotionType.ROLL:
                    rollCamera();
                    break;
                case CameraMotionType.ZOOM:
                    dragZoom();
                    break;
                default: break;
            }
        }

        protected void dragArc()
        {
            mRotationSum *= mRotation;
            updateCameraEyeUp(true, true);
        }

        protected void dragFirstPerson()
        {
            vec3 pan = new vec3(0.0f, 0.0f, 0.0f);
            computePan(ref pan);
            mCamera.setCenter(pan + mCamera.getCenter());
            mCamera.update();
            freezeTransform();
        }

        protected void dragPan()
        {
            vec3 pan = new vec3(0.0f, 0.0f, 0.0f);
            computePan(ref pan);
            mCamera.setCenter(pan + mCamera.getCenter());
            mCamera.setEye(pan + mCamera.getEye());
            mCamera.update();
            freezeTransform();
        }

        protected void dragZoom()
        {
            vec2 dir = mClickPoint - mPrevClickPoint;
            float ax = Math.Abs(dir.x);
            float ay = Math.Abs(dir.y);

            if (ay >= ax)
            {
                setScrollDirection(dir.y <= 0);
            }
            else
            {
                setScrollDirection(dir.x <= 0);
            }

            updateCameraEyeUp(true, false);
        }

        protected void freezeTransform()
        {
            if (mCamera != null)
            {
                mRotationSum = (new quat(mCamera.getMatrix())).Inverse;
                mTranslateLength = (mCamera.getEye() - mCamera.getCenter()).Length;
            }
        }

        public Camera getCamera()
        {
            return mCamera;
        }

        public CameraMotionType getMotionLeftClick()
        {
            return mCameraMotionLeftClick;
        }

        public CameraMotionType getMotionMiddleClick()
        {
            return mCameraMotionMiddleClick;
        }

        public CameraMotionType getMotionRightClick()
        {
            return mCameraMotionRightClick;
        }

        public CameraMotionType getMotionScroll()
        {
            return mCameraMotionScroll;
        }

        protected void rollCamera()
        {
            vec2 delta = mClickPoint - mPrevClickPoint;
            char quad = clickQuadrant(mClickPoint.x, mClickPoint.y);
            switch (quad)
            {
                case '1':
                    delta.y = -delta.y;
                    delta.x = -delta.x;
                    break;
                case '2':
                    delta.x = -delta.x;
                    break;
                case '3':
                    break;
                case '4':
                    delta.y = -delta.y;
                    break;
                default: break;
            }

            vec3 axis = (mCamera.getCenter() - mCamera.getEye()).NormalizedSafe;
            float angle = mRollScale * mSpeed * (delta.x + delta.y + mRollSum);
            quat rot = quat.FromAxisAngle(angle, axis);
            mCamera.setUp(rot * mCamera.getUp());
            mCamera.update();
            freezeTransform();
            mRollSum = 0;
        }
        
        protected void scroll()
        {
            switch (mCameraMotionScroll)
            {
                case CameraMotionType.ROLL:
                    rollCamera();
                    break;
                case CameraMotionType.ZOOM:
                    updateCameraEyeUp(true, false);
                    break;
                default: break;
            }
        }

        public void setCamera(Camera c)
        {
            mCamera = c;
            freezeTransform();
        }

        public void setClickPoint(double x, double y)
        {
            mPrevClickPoint = mClickPoint;
            mClickPoint.x = (float)x;
            mClickPoint.y = (float)y;
        }

        public void setLeftClicked(bool value)
        {
            mIsLeftClick = value;
        }

        public void setMiddleClicked(bool value)
        {
            mIsMiddleClick = value;
        }

        public void setMotionLeftClick(CameraMotionType motion)
        {
            mCameraMotionLeftClick = motion;
        }

        public void setMotionMiddleClick(CameraMotionType motion)
        {
            mCameraMotionMiddleClick = motion;
        }

        public void setMotionRightClick(CameraMotionType motion)
        {
            mCameraMotionRightClick = motion;
        }

        public void setMotionScroll(CameraMotionType motion)
        {
            mCameraMotionScroll = motion;
        }

        public void setRightClicked(bool value)
        {
            mIsRightClick = value;
        }

        public void setScreenSize(float width, float height)
        {
            if (width > 1 && height > 1)
            {
                mWidth = width;
                mHeight = height;
            }
        }

        public void setScrollDirection(bool up)
        {
            mIsScrolling = true;
            float inc = mSpeed * (up ? -1.0f : 1.0f);
            mZoomSum += inc;
            mRollSum += inc;
        }

        public void setSpeed(float s)
        {
            mSpeed = s;
        }

        public void update()
        {
            bool isClick = mIsLeftClick || mIsMiddleClick || mIsRightClick;

            if (!mIsDragging)
            {
                if (isClick)
                {
                    mIsDragging = true;
                    computePointOnSphere(mClickPoint, ref mStartVector);
                }
                else if (mIsScrolling)
                {
                    scroll();
                    mIsScrolling = false;
                }
            }
            else
            {
                if (isClick)
                {
                    drag();
                }
                else
                {
                    mIsDragging = false;
                }
            }
        }

        protected void updateCameraEyeUp(bool eye, bool up)
        {
            if (eye)
            {
                vec3 e = new vec3(0.0f, 0.0f, 0.0f);
                computeCameraEye(ref e);
                mCamera.setEye(e);
            }
            if (up)
            {
                vec3 u = new vec3(0.0f, 0.0f, 0.0f);
                computeCameraUp(ref u);
                mCamera.setUp(u);
            }
            mCamera.update();
        }
    }
}